<?php
//**                          MAIN API                      **//
//**                                                        **//
//**                                                        **//

//*******************************************************************//
//** DATABASE ABSTRACTION LAYER - enables execution outside Joomla **//
//*******************************************************************//

interface iCmealsDB {
	/**
	 * Add common prefix to table names
	 */
	public function addPrefix($tableName);
	
	/**
	 * Quote a value for a query
	 */
	public function quote($value);
	
	/**
	 * Do a read query returning a list of objects
	 */
	public function readQuery($query);
	
	/**
	 * Do a read query returning a single object
	 */
	public function readQuerySingle($query);
	
	/**
	 * Do a read query returning just a value (such as a row count)
	 */
	public function readQueryValue($query);
	
	/**
	 * Do a write query, either create, update, or delete
	 */
	public function writeQuery($query);
}

class cmealsDBJoomla implements iCmealsDB {
	
	private $db;
	
	function __construct() {
		$this->db =& JFactory::getDBO();
	}
	
	public function addPrefix($tableName) {
		return "#__cmeals_".$tableName;
	}
	
	public function quote($value) {
		if (is_null($value)) {
			return "NULL";
		}
		return $this->db->Quote($value);
	}
	
	public function readQuery($query) {
		$this->db->setQuery($query);
		$retObj = new cmealsDBResult();
		if ($res =& $this->db->loadObjectList()) {
			$retObj->result = $res;
		}
		else {
			$errorMsg = $this->db->getErrorMsg();
			if ($errorMsg) {
				$retObj->isError = true;
				$retObj->errorMsg = $errorMsg;
			}
			else {
				$retObj->result = array();
			}
		}
		return $retObj;
	}
	
	public function readQuerySingle($query) {
		$retObj = $this->readQuery($query);
		if (!$retObj->isError) {
			// check array has one entry and replace result with this
			if (is_array($retObj->result) && (count($retObj->result) > 0)) {
				$retObj->result = $retObj->result[0];
			}
			else {
				$retObj->result = null;
			}
		}
		return $retObj;
	}

	public function readQueryValue($query) {
		$this->db->setQuery($query);
		$retObj = new cmealsDBResult();
		if ($res = $this->db->loadResult()) {
			$retObj->result = $res;
		}
		else {
			$errorMsg = $this->db->getErrorMsg();
			if ($errorMsg) {
				$retObj->isError = true;
				$retObj->errorMsg = $errorMsg;
			}
			else {
				$retObj->result = null;
			}
		}
		return $retObj;
	}
	
	public function writeQuery($query) {
		$this->db->setQuery($query);
		$this->db->query();
		$retObj = new cmealsDBResult();
		if ($errorMsg = $this->db->getErrorMsg()) {
			$retObj->isError = true;
			$retObj->errorMsg = $errorMsg;
		}
		else {
			$retObj->result = $this->db->getAffectedRows();
		}
		return $retObj;
	}
	
}

class cmealsDBResult {
	public $isError = false;
	public $result = null;
	public $errorMsg = null;
}


//*******************************************************************//
//** ERROR HANDLING ABSTRACTION LAYER - for exec. outside Joomla   **//
//*******************************************************************//

interface iCmealsError {
	public function handleInfo($code, $details = "");
	public function handleWarning($code, $details = "");
	public function handleError($code, $details = "");
}

class cmealsErrorJoomla implements iCmealsError {
	
	public function handleInfo($code, $details = "") {
		JError::raiseNotice("200", JText::_($code)." ".$details);
	}
	
	public function handleWarning($code, $details = "") {
		JError::raiseWarning("200", JText::_($code)." ".$details);
	}
	
	public function handleError($code, $details = "") {
		JError::raiseWarning("500", JText::_($code)." ".$details);
	}
}

//*******************************************************************//
//** CONFIGURATION ABSTRACTION LAYER - for exec. outside Joomla    **//
//*******************************************************************//

interface iCmealsConfig {
	public function getProperty($name);
}

class cmealsConfigJoomla implements iCmealsConfig {
	
	public function getProperty($name) {
		$params = JComponentHelper::getParams("com_communalmeals");
		return $params->get($name);
	}
}

//*******************************************************************//
//**                           Common                              **//
//*******************************************************************//

/**
 * Dummy "anonymous" class
 */
class cmeals_std_class {
}

//*******************************************************************//
//**                            API                                **//
//*******************************************************************//

/**
 * Main class containing all static methods of API
 */
class CMeals {
	
	// Constants - Event status
	const STATUS_PLANNING = "00";
	const STATUS_OPEN = "20";
	const STATUS_CLOSED = "40";
	const STATUS_BILLING = "60";
	const STATUS_ARCHIVE = "80";
	
	// Constants - Event type
	const TYPE_COMMUNAL_MEAL = "CM";

	// Private constants
	private static $NOT_USED = "_NOT_USED_";
	
	// Static instance of iCmealsDB for DB access
	private static $db = null;

	// Static instance of iCmealsConfig for configuration
	private static $cfg = null;

	// Static instance of iCmealsError for error handling
	private static $err = null;

	/**
	 * Set the DB access class - primarily for unit testing outside of joomla
	 * @param $db an instance of iCmealsDB
	 */
	public static function setDB($p_db) {
		self::$db = $p_db;
	}
	
	/**
	 * Set the configuration class - primarily for unit testing outside of joomla
	 * @param $cfg an instance of iCmealsConfig
	 */
	public static function setConfig($p_cfg) {
		self::$cfg = $p_cfg;
	}
	
	/**
	 * Set the error handler class - primarily for unit testing outside of joomla
	 * @param $err an instance of iCmealsError
	 */
	public static function setErrorHandler($p_err) {
		self::$err = $p_err;
	}
	
	public static function getErrorHandler() {
		self::checkInit();
		return self::$err;
	}
	
	/**
	 * By default set DB, config and error handler to joomla implementation
	 */
	private static function checkInit() {
		if (is_null(self::$db)) {
			self::$db = new cmealsDBJoomla();
		}
		if (is_null(self::$cfg)) {
			self::$cfg = new cmealsConfigJoomla();
		}
		if (is_null(self::$err)) {
			self::$err = new cmealsErrorJoomla();
		}
	}


	//*******************************************************************//
	//**                         Event Group                           **//
	//*******************************************************************//

	/**
	 * Read event group list (Those marked for deletion will be ignored)
	 * @param $orderBy optional (supply anything that can follow an sql order by statement)
	 * @param $offset optional 
	 * @param $rowCount optional (offset and rowCount must both be supplied or not)
	 * @return array of event group objects (empty array if nothing found)
	 */
	public static function readEventGroupList($orderBy=null, $offset=null, $rowCount=null) {
		self::checkInit();
		
		$query = "SELECT * FROM ".self::$db->addPrefix("eventgroup").
			" WHERE deleted IS NULL";
		
		if (!is_null($orderBy)) {
			$query .= " ORDER BY ".$orderBy;
		}
		
		if ((!is_null($offset)) && (!is_null($rowCount))) {
			$query .= " LIMIT ".intval($offset).", ".intval($rowCount);
		}
		
		$res = self::$db->readQuery($query);
		
		if ($res->isError) {
			self::$err->handleError("EVENTGROUP_READ_ERROR", $res->errorMsg);
			return array();
		}
		return $res->result;		
	}
	
	/**
	 * Get list of event groups that are marked for deletion earlier than a given date
	 * @param $deletedBeforeDate
	 * @return list of event group objects
	 */
	public static function readDeletedEventGroupList($deletedBeforeDate=null) {
		self::checkInit();
		
		$query = "SELECT * FROM ".self::$db->addPrefix("eventgroup").
			(is_null($deletedBeforeDate) ? " WHERE deleted IS NOT NULL":" WHERE deleted < ".self::$db->quote($deletedBeforeDate));
		
		$res = self::$db->readQuery($query);
		
		if ($res->isError) {
			self::$err->handleError("EVENTGROUP_READ_ERROR", "(reading deleted before '".$deletedBeforeDate."' ".$res->errorMsg);
			return false;
		}
		return $res->result;		
	}
	
	/**
	 * Get list of event groups where ALL events are in archive status
	 * @return list of event group objects
	 */
	public static function readEventGroupListByMinimumStatus($status) {
		self::checkInit();
		
		$query = "SELECT * FROM ".self::$db->addPrefix("eventgroup").
			" WHERE id NOT IN ".
				"(SELECT DISTINCT group_id FROM ".self::$db->addPrefix("event").
				" WHERE status < ".self::$db->quote($status).")";
		
		$res = self::$db->readQuery($query);
		
		if ($res->isError) {
			self::$err->handleError("EVENTGROUP_READ_ERROR", "(reading by minimum status: ".$status.") ".$res->errorMsg);
			return false;
		}
		return $res->result;		
	}
	
	/**
	 * Count total number of event groups (not marked for deletion)
	 * @return integer
	 */
	public static function countEventGroups() {
		self::checkInit();
		
		$query = "SELECT count(*) FROM ".self::$db->addPrefix("eventgroup").
			" WHERE deleted IS NULL";
		
		$res = self::$db->readQueryValue($query);
		
		if ($res->isError) {
			self::$err->handleError("EVENTGROUP_READ_ERROR", $res->errorMsg);
			return 0;
		}
		else {
			if (is_null($res->result)) {
				return 0;
			}
		}
		return $res->result;		
	}
	
	/**
	 * Read event group
	 * @param $id
	 * @return an event group object (null if nothing found or event group is marked for deletion)
	 */
	public static function readEventGroup($id) {
		self::checkInit();

		$query = "SELECT * FROM ".self::$db->addPrefix("eventgroup").
			" WHERE id=".self::$db->quote( $id ).
			" AND deleted IS NULL";

		$res = self::$db->readQuerySingle($query);
		if ($res->isError) {
			self::$err->handleError("EVENTGROUP_READ_ERROR", "id=".$id.". ".$res->errorMsg);
			return null;
		}

		return $res->result;
	}
	
	/**
	 * Create an event group
	 * @param $eventGroup object with key field [id]
	 */
	public static function createEventGroup($eventGroup) {
		self::checkInit();
		if (is_null($eventGroup->id)) {
			self::$err->handleError("NULL_FIELD_ERROR", print_r($eventGroup, true));
			return null;
		}
		
		$query = "INSERT INTO ".self::$db->addPrefix("eventgroup").
			" (id, description, description_html, organiser, organiser_description,".
			" accountant, accountant_description, deleted, changed, created)".
			" VALUES (".self::$db->quote( $eventGroup->id ).", ".
			self::$db->quote( $eventGroup->description ).", ".
			self::$db->quote( $eventGroup->description_html ).", ".
			self::$db->quote( $eventGroup->organiser ).", ".
			self::$db->quote( $eventGroup->organiser_description ).", ".
			self::$db->quote( $eventGroup->accountant ).", ".
			self::$db->quote( $eventGroup->accountant_description ).", NULL, NULL, NULL)";
			
		$res = self::$db->writeQuery($query);
		if ($res->isError) {
			self::$err->handleError("EVENTGROUP_CREATE_ERROR", $res->errorMsg);
			return null;
		}
	}

	/**
	 * Update an event group (cannot be used on event groups marked for deletion!)
	 * @param $eventGroup object with key field [id]
	 */
	public static function updateEventGroup($eventGroup) {
		self::checkInit();
		if (is_null($eventGroup->id)) {
			self::$err->handleError("NULL_FIELD_ERROR", print_r($eventGroup, true));
			return null;
		}
		if (is_null(self::readEventGroup($eventGroup->id))) {
			self::$err->handleError("EVENTGROUP_UPDATE_ERROR", "No such event group ".print_r($eventGroup, true));
			return null;
		}
		
		$query = "UPDATE ".self::$db->addPrefix("eventgroup").
			" SET description=".self::$db->quote( $eventGroup->description ).
			", description_html=".self::$db->quote( $eventGroup->description_html ).
			", organiser=".self::$db->quote( $eventGroup->organiser ).
			", organiser_description=".self::$db->quote( $eventGroup->organiser_description ).
			", accountant=".self::$db->quote( $eventGroup->accountant ).
			", accountant_description=".self::$db->quote( $eventGroup->accountant_description ).
			", deleted=NULL, changed=NULL WHERE id=".self::$db->quote( $eventGroup->id );
			
		$res = self::$db->writeQuery($query);
		if ($res->isError) {
			self::$err->handleError("EVENTGROUP_UPDATE_ERROR", $res->errorMsg);
			return null;
		}
	}

	/**
	 * Update an event group with only changed fields
	 * @param $eventGroup with required key field [id] and changed fields
	 */
	public static function updateEventGroupDelta($eventGroup) {
		self::checkInit();
		if (is_null($eventGroup->id)) {
			self::$err->handleError("NULL_FIELD_ERROR", print_r($eventGroup, true));
			return null;
		}
		$oldEventGroup = self::readEventGroup($eventGroup->id);
		if (is_null($eventGroup->description)) $eventGroup->description = $oldEventGroup->description;
		if (is_null($eventGroup->description_html)) $eventGroup->description_html = $oldEventGroup->description_html;
		if (is_null($eventGroup->organiser)) $eventGroup->organiser = $oldEventGroup->organiser;
		if (is_null($eventGroup->organiser_description)) $eventGroup->organiser_description = $oldEventGroup->organiser_description;
		if (is_null($eventGroup->accountant)) $eventGroup->accountant = $oldEventGroup->accountant;
		if (is_null($eventGroup->accountant_description)) $eventGroup->accountant_description = $oldEventGroup->accountant_description;
		self::updateEventGroup($eventGroup);
	}
		
	/**
	 * Delete an event group. If event group has processing events, it will only be marked for deletion and hidden.
	 * @param $eventGroup object with key field [id]
	 */
	public static function deleteEventGroup($eventGroup) {
		self::deleteEventGroupForced($eventGroup, false);
	}
	
	/**
	 * PRIVATE Delete an event group with option to "use force"
	 * @param $eventGroup object with key field [id]
	 * @param $useForce boolean - if true then delete despite events in process etc.
	 */
	private static function deleteEventGroupForced($eventGroup, $useForce) {
		self::checkInit();
		if (is_null($eventGroup->id)) {
			self::$err->handleError("NULL_FIELD_ERROR", print_r($eventGroup, true));
			return null;
		}
		
		// Check whether event group can be deleted
		$canDeletePermanently = $useForce || !(self::eventGroupHasProcessingEvents($eventGroup->id));
				
		// If there are no events in status open, closed, billing etc. then delete all related data
		if ($canDeletePermanently) {
			// Delete crew
			self::deleteCrewByFields($eventGroup->id);
			
			// Delete events
			$eventList = self::readEventList($eventGroup->id);
			foreach ($eventList as $event) {
				self::deleteEvent($event);
			}
			
			// Delete the event group itself
			$query = "DELETE FROM ".self::$db->addPrefix("eventgroup").
				" WHERE id=".self::$db->quote( $eventGroup->id );
		}
		else {
			// Otherwise mark eventgroup for deletion
			$query = "UPDATE ".self::$db->addPrefix("eventgroup").
				" SET deleted=".self::$db->quote( date("Y-m-d") ).
				" WHERE id=".self::$db->quote( $eventGroup->id );
		}
		
		$res = self::$db->writeQuery($query);
		if ($res->isError) {
			$errCode = $canDeletePermanently ? "EVENTGROUP_DELETE_ERROR" : "EVENTGROUP_MARK_FOR_DELETION_ERROR";
			self::$err->handleError($errCode, $res->errorMsg);
			return null;
		}
	}
	
	/**
	 * Check whether event group has any events that cannot be safely deleted 
	 * @param $group_id event group id
	 * @return boolean
	 */
	public static function eventGroupHasProcessingEvents($group_id) {
		self::checkInit();
		// Check whether any events are in open, closed, or billing state
		$eventList = self::readEventList($group_id);
		$hasProcessingEvents = false;
		foreach ($eventList as $event) {
			if ($event->status == self::STATUS_OPEN || $event->status == self::STATUS_CLOSED || $event->status == self::STATUS_BILLING) {
				$hasProcessingEvents = true;
			}
		}
		return $hasProcessingEvents;
	}
	
	/**
	 * Housekeeping function. Should be run by a scheduler or similar.
	 * 
	 * Deletes event groups and all related information if
	 * 1) Event group is marked for deletion at least x days ago
	 * or
	 * 2) All events in event group are in archive status and event happened at least x days ago
	 * 
	 * The number of days, x, is determined from configuration
	 */
	public static function cleanupEventGroups() {
		self::checkInit();
		
		// Find and delete event groups marked for deletion where the due date has expired
		$daysToKeep = self::$cfg->getProperty("days_to_keep_archive");
		if (!is_numeric($daysToKeep)) {
			$daysToKeep = 365;
		}
		
		$deletedBeforeDate = date("Y-m-d", time() - ($daysToKeep * 86400));
		$deletedEventGroupList = self::readDeletedEventGroupList($deletedBeforeDate);
		foreach ($deletedEventGroupList as $eventGroup) {
			self::deleteEventGroupForced($eventGroup, true);
		}
		
		// Find and delete event groups where all events have been archived and due date expired 
		$allArchivedEventGroupList = self::readEventGroupListByMinimumStatus(self::STATUS_ARCHIVE);
		foreach ($allArchivedEventGroupList as $eventGroup) {
			self::deleteEventGroupForced($eventGroup, true);
		}
	}
	
	//*******************************************************************//
	//**                            Event                              **//
	//*******************************************************************//

	/**
	 * Get event count related to an event group
	 * @param $group_id
	 * @return integer
	 */
	public static function countEvents($group_id) {
		self::checkInit();
		
		$query = "SELECT count(*) FROM ".self::$db->addPrefix("event").
			" WHERE group_id=".self::$db->quote( $group_id );
				
		$res = self::$db->readQueryValue($query);
		
		if ($res->isError) {
			self::$err->handleError("EVENT_READ_ERROR", $res->errorMsg);
			return 0;
		}
		else {
			if (is_null($res->result)) {
				return 0;
			}
		}
		return $res->result;		
	}
	
	/**
	 * Read event list related to an event group (ordered by event date and type)
	 * @param $group_id
	 * @return array of event objects (empty array if nothing found)
	 */
	public static function readEventList($group_id) {
		self::checkInit();
		
		$query = "SELECT * FROM ".self::$db->addPrefix("event").
			" WHERE group_id=".self::$db->quote( $group_id ).
			" ORDER BY date, type";
		
		$res = self::$db->readQuery($query);
		
		if ($res->isError) {
			self::$err->handleError("EVENT_READ_ERROR", "group_id=".$group_id.". ".$res->errorMsg);
			return array();
		}
		return $res->result;		
	}
	
	/**
	 * Read event list for a date range (ordered by event date and type)
	 * @param $startDate inclusive (format YYYY-MM-DD)
	 * @param $endDate inclusive (format YYYY-MM-DD) (optional)
	 * @param $type (optional)
	 * @param $limit (optional)
	 * @return array of event objects (empty array if nothing found)
	 */
	public static function readEventListByDate($startDate, $endDate=NULL, $type=NULL, $limit=NULL) {
		self::checkInit();
		
		$query = "SELECT e.* FROM ".self::$db->addPrefix("event").
			" AS e JOIN ".self::$db->addPrefix("eventgroup").
			" AS g ON e.group_id=g.id".
			" WHERE e.date>=".self::$db->quote( $startDate ).
			(is_null($endDate) ? "":" AND e.date<=".self::$db->quote( $endDate )).
			(is_null($type) ? "":" AND e.type=".self::$db->quote( $type )).
			" AND g.deleted IS NULL".
			" ORDER BY e.date, e.type".
			(is_null($limit) ? "":" LIMIT ".$limit);
		
		$res = self::$db->readQuery($query);
		
		if ($res->isError) {
			self::$err->handleError("EVENT_READ_ERROR", "startDate=".$startDate.", endDate=".$endDate.". ".$res->errorMsg);
			return array();
		}
		return $res->result;		
	}
	
	/**
	 * Read event
	 * @param $group_id
	 * @param $event_id
	 * @return an event object (null if nothing found)
	 */
	public static function readEvent($id, $group_id) {
		self::checkInit();

		$query = "SELECT * FROM ".self::$db->addPrefix("event").
			" WHERE id=".self::$db->quote( $id ).
			" AND group_id=".self::$db->quote( $group_id );
		
		$res = self::$db->readQuerySingle($query);
		if ($res->isError) {
			self::$err->handleError("EVENT_READ_ERROR", "id=".$id.", group_id=".$group_id.". ".$res->errorMsg);
			return null;
		}

		return $res->result;
	}
	
	/**
	 * Create an event
	 * @param $event object with key fields and required fields [id, group_id, type, subtype, status, date]
	 */
	public static function createEvent($event) {
		self::checkInit();
		if (is_null($event->id) || is_null($event->group_id) || is_null($event->type) || is_null($event->subtype) || is_null($event->status) || is_null($event->date)) {
			self::$err->handleError("NULL_FIELD_ERROR", print_r($event, true));
			return null;
		}
		
		$query = "INSERT INTO ".self::$db->addPrefix("event").
			" (id, group_id, type, subtype, status, date, time, description, description_html, closing_date, closing_time,".
			" organiser, organiser_description, accountant, accountant_description, changed, created)".
			" VALUES (".self::$db->quote( $event->id ).", ".
			self::$db->quote( $event->group_id ).", ".
			self::$db->quote( $event->type ).", ".
			self::$db->quote( $event->subtype ).", ".
			self::$db->quote( $event->status ).", ".
			self::$db->quote( $event->date ).", ".
			self::$db->quote( $event->time ).", ".
			self::$db->quote( $event->description ).", ".
			self::$db->quote( $event->description_html ).", ".
			self::$db->quote( $event->closing_date ).", ".
			self::$db->quote( $event->closing_time ).", ".
			self::$db->quote( $event->organiser ).", ".
			self::$db->quote( $event->organiser_description ).", ".
			self::$db->quote( $event->accountant ).", ".
			self::$db->quote( $event->accountant_description ).", NULL, NULL)";
			
		$res = self::$db->writeQuery($query);
		if ($res->isError) {
			self::$err->handleError("EVENT_CREATE_ERROR", $res->errorMsg);
			return null;
		}
	}

	/**
	 * Update an event
	 * @param $event object with key fields and required fields [id, group_id, type, subtype, status, date]
	 */
	public static function updateEvent($event) {
		self::checkInit();
		if (is_null($event->id) || is_null($event->group_id) || is_null($event->type) || is_null($event->subtype) || is_null($event->status) || is_null($event->date)) {
			self::$err->handleError("NULL_FIELD_ERROR", print_r($event, true));
			return null;
		}
		
		$oldEvent = self::readEvent($event->id, $event->group_id);
		if (!self::checkEventStatusChange($oldEvent->status, $event->status)) {
			return null;
		}
		
		$query = "UPDATE ".self::$db->addPrefix("event").
			" SET type=".self::$db->quote( $event->type ).
			", subtype=".self::$db->quote( $event->subtype ).
			", status=".self::$db->quote( $event->status ).
			", date=".self::$db->quote( $event->date ).
			", time=".self::$db->quote( $event->time ).
			", description=".self::$db->quote( $event->description ).
			", description_html=".self::$db->quote( $event->description_html ).
			", closing_date=".self::$db->quote( $event->closing_date ).
			", closing_time=".self::$db->quote( $event->closing_time ).
			", organiser=".self::$db->quote( $event->organiser ).
			", organiser_description=".self::$db->quote( $event->organiser_description ).
			", accountant=".self::$db->quote( $event->accountant ).
			", accountant_description=".self::$db->quote( $event->accountant_description ).
			", changed=NULL WHERE id=".self::$db->quote( $event->id ).
			" AND group_id=".self::$db->quote( $event->group_id );
			
		$res = self::$db->writeQuery($query);
		if ($res->isError) {
			self::$err->handleError("EVENT_UPDATE_ERROR", $res->errorMsg);
			return null;
		} 
		
		// Create actual signups from automatic signups
		if ($oldEvent->status == self::STATUS_PLANNING && $event->status == self::STATUS_OPEN) {
			self::createSignUpsFromAutoSignUps($event);
		}
	}

	/**
	 * Update an event with only changed fields
	 * @param $event with required key fields [id, group_id] and changed fields
	 */
	public static function updateEventDelta($event) {
		self::checkInit();
		if (is_null($event->id) || is_null($event->group_id)) {
			self::$err->handleError("NULL_FIELD_ERROR", print_r($event, true));
			return null;
		}
		$oldEvent = self::readEvent($event->id, $event->group_id);
		if (!is_null($event->status)) {
			if (!self::checkEventStatusChange($oldEvent->status, $event->status)) {
				return null;
			}
		}
		if (is_null($event->type)) $event->type = $oldEvent->type;
		if (is_null($event->subtype)) $event->subtype = $oldEvent->subtype;
		if (is_null($event->status)) $event->status = $oldEvent->status;
		if (is_null($event->date)) $event->date = $oldEvent->date;
		if (is_null($event->time)) $event->time = $oldEvent->time;
		if (is_null($event->description)) $event->description = $oldEvent->description;
		if (is_null($event->description_html)) $event->description_html = $oldEvent->description_html;
		if (is_null($event->closing_date)) $event->closing_date = $oldEvent->closing_date;
		if (is_null($event->closing_time)) $event->closing_time = $oldEvent->closing_time;
		if (is_null($event->organiser)) $event->organiser = $oldEvent->organiser;
		if (is_null($event->organiser_description)) $event->organiser_description = $oldEvent->organiser_description;
		if (is_null($event->accountant)) $event->accountant = $oldEvent->accountant;
		if (is_null($event->accountant_description)) $event->accountant_description = $oldEvent->accountant_description;
		self::updateEvent($event);
	}
		
	/**
	 * Update status on one event or all events in event group
	 * @param $status
	 * @param $group_id
	 * @param $event_id optional - if not present all events in group are affected
	 */
	public static function updateEventStatus($status, $group_id, $event_id=null) {
		self::checkInit();
		if (is_null($status) || is_null($group_id)) {
			self::$err->handleError("NULL_FIELD_ERROR");
			return null;
		}
		
		// Check status of event(s)
		if (is_null($event_id)) {
			$oldEventList = self::readEventList($group_id);
		}
		else {
			$oldEventList = array(self::readEvent($event_id, $group_id));
		}
		foreach ($oldEventList as $oldEvent) {
			if (!self::checkEventStatusChange($oldEvent->status, $status)) {
				return null;
			}
		}
		
		$query = "UPDATE ".self::$db->addPrefix("event").
			" SET status=".self::$db->quote( $status ).
			", changed=NULL WHERE group_id=".self::$db->quote( $group_id ).
			(is_null($event_id) ? "":" AND id=".self::$db->quote( $event_id ));
			
		$res = self::$db->writeQuery($query);
		if ($res->isError) {
			self::$err->handleError("EVENT_UPDATE_ERROR", $res->errorMsg);
			return null;
		} 
	}	
	
	public static function checkEventStatusChange($oldStatus, $newStatus) {
		if (($oldStatus >= self::STATUS_OPEN && $newStatus <= self::STATUS_PLANNING) ||
			($oldStatus >= self::STATUS_BILLING && $newStatus < $oldStatus) ||
			($oldStatus <= self::STATUS_PLANNING && $newStatus > self::STATUS_OPEN) ||
			($oldStatus < self::STATUS_CLOSED && $newStatus >= self::STATUS_BILLING) ||
			($oldStatus < self::STATUS_BILLING && $newStatus >= self::STATUS_ARCHIVE)) {
			self::$err->handleError("ILLEGAL_STATUS_ERROR", "Cannot go from '".$oldStatus."' to '".$newStatus."'");
			return false;
		}
		else {
			return true;
		}
	}
	
	/**
	 * Housekeeping function. Should be run by a scheduler or similar.
	 * 
	 * Eligible events for archiving are part of an event group where 
	 * ALL events are in billing state
	 * and all the events have taken place more than a certain time ago.
	 * 
	 * After archiving, ALL events in relevant event groups will be in 
	 * status archive and signup information will be deleted.
	 */
	public static function archiveEvents() {
		self::checkInit();
		
		// Find date up to which events that are in billing status can be archived
		$daysUntilArchive = self::$cfg->getProperty("days_until_event_archive");
		if (!is_numeric($daysUntilArchive)) {
			$daysUntilArchive = 30;
		}
		
		$dateArchiveBefore = date("Y-m-d", time() - ($daysUntilArchive * 86400));
		
		// Find eligible event groups
		$eventGroupList = self::readEventGroupListByMinimumStatus(self::STATUS_BILLING);

		foreach ($eventGroupList as $eventGroup) {
			$eventList = self::readEventList($eventGroup->id);
			$archiveGroup = true; // Initial assumption
			$alreadyArchived = true; // Initial assumption
			foreach ($eventList as $event) {
				if ($event->date >= $dateArchiveBefore) {
					$archiveGroup = false;
				}
				if ($event->status < self::STATUS_ARCHIVE) {
					$alreadyArchived = false;
				}
			}
			if ($archiveGroup && !$alreadyArchived) {
				// Change all events in group to status archive
				self::updateEventStatus(self::STATUS_ARCHIVE, $eventGroup->id);
				// Delete signup info for the events
				self::deleteSignUpByFields($eventGroup->id);
			}
		}
	}
	
	/**
	 * Permanently delete an event. (Only possible if status is planning or archive)
	 * @param $event object with key fields [id, group_id]
	 */
	public static function deleteEvent($event) {
		self::checkInit();
		if (is_null($event->id) || is_null($event->group_id)) {
			self::$err->handleError("NULL_FIELD_ERROR", print_r($event, true));
			return null;
		}
		
		$query = "DELETE FROM ".self::$db->addPrefix("event").
			" WHERE id=".self::$db->quote( $event->id ).
			" AND group_id=".self::$db->quote( $event->group_id ).
			" AND (status='".self::STATUS_PLANNING."' OR status='".self::STATUS_ARCHIVE."')";
			
		$res = self::$db->writeQuery($query);
		if ($res->isError) {
			self::$err->handleError("EVENT_DELETE_ERROR", $res->errorMsg);
			return null;
		}
		else if ($res->result == 1) {
			// Delete crew
			self::deleteCrewByFields($event->group_id, $event->id);
			
			// Delete signups
			self::deleteSignUpByFields($event->group_id, $event->id);			
		}
	}

	//*******************************************************************//
	//**                            Crew                               **//
	//*******************************************************************//
	
	/**
	 * Read crew list (ordered by member id)
	 * @param $group_id
	 * @param $event_id (optional)
	 * @return array of crew objects (empty array if nothing found)
	 */
	public static function readCrewList($group_id, $event_id=null) {
		self::checkInit();
		
		$query = "SELECT c.*,m.name FROM ".self::$db->addPrefix("crew")." AS c LEFT JOIN ".
			self::$db->addPrefix("member")." AS m ON c.member_id = m.id".
			" WHERE c.group_id=".self::$db->quote( $group_id ).
			(is_null($event_id) ? "" : " AND c.event_id=".self::$db->quote( $event_id )).
			" ORDER BY c.member_id";
		
		$res = self::$db->readQuery($query);
		if ($res->isError) {
			self::$err->handleError("CREW_READ_ERROR", $res->errorMsg);
			return array();
		}
		return $res->result;		
	}
	
	/**
	 * Create a crew assignment
	 * @param $crew object with key fields [member_id, group_id, event_id] (event_id is optional)
	 */
	public static function createCrew($crew) {
		self::checkInit();
		if (is_null($crew->member_id) || is_null($crew->group_id)) {
			self::$err->handleError("NULL_FIELD_ERROR", print_r($crew, true));
			return null;
		}
		if (is_null($crew->event_id)) {
			$crew->event_id = self::$NOT_USED;
		}
		
		$query = "INSERT INTO ".self::$db->addPrefix("crew").
			" (member_id, group_id, event_id, member_description, changed, created)".
			" VALUES (".self::$db->quote( $crew->member_id ).", ".
			self::$db->quote( $crew->group_id ).", ".
			self::$db->quote( $crew->event_id ).", ".
			self::$db->quote( $crew->member_description ).", NULL, NULL)";
			
		$res = self::$db->writeQuery($query);
		if ($res->isError) {
			self::$err->handleError("CREW_CREATE_ERROR", $res->errorMsg);
			return null;
		}
	}
	
	/**
	 * Permanently delete a crew assignment.
	 * @param $crew object with key fields [member_id, group_id, event_id] (event_id is optional)
	 */
	public static function deleteCrew($crew) {
		self::checkInit();
		if (is_null($crew->member_id) || is_null($crew->group_id)) {
			self::$err->handleError("NULL_FIELD_ERROR", print_r($crew, true));
			return null;
		}
		return self::deleteCrewByFields($crew->group_id, $crew->event_id, $crew->member_id);
	}
	
	/**
	 * Permanently delete a crew assignment.
	 * @param $group_id
	 * @param $event_id optional
	 * @param $member_id optional
	 */
	private static function deleteCrewByFields($group_id, $event_id=null, $member_id=null) {
		self::checkInit();
		if (is_null($group_id)) {
			self::$err->handleError("NULL_FIELD_ERROR", print_r($group_id, true));
			return null;
		}
		if (is_null($event_id)) {
			$event_id = self::$NOT_USED;
		}
				
		$query = "DELETE FROM ".self::$db->addPrefix("crew").
			" WHERE group_id=".self::$db->quote( $group_id ).
			" AND event_id=".self::$db->quote( $event_id );
		
		if (!is_null($member_id)) {
			$query .= " AND member_id=".self::$db->quote( $member_id );
		}
		
		$res = self::$db->writeQuery($query);
		if ($res->isError) {
			self::$err->handleError("CREW_DELETE_ERROR", $res->errorMsg);
			return null;
		}
	}
	
	/**
	 * Check if member is part of the crew for an event group
	 * @param $member_id
	 * @param $group_id
	 * @return boolean
	 */
	public static function isEventGroupCrew($member_id, $group_id) {
		self::checkInit();
		
		$query = "SELECT count(*) FROM ".self::$db->addPrefix("crew").
			" WHERE member_id=".self::$db->quote( $member_id ).
			" AND group_id=".self::$db->quote( $group_id );
		
		$res = self::$db->readQueryValue($query);
		if ($res->isError) {
			self::$err->handleError("CREW_READ_ERROR", $res->errorMsg);
			return array();
		}
		return ($res->result > 0);
	}
	
	/**
	 * Check if member is part of the crew for a particular event
	 * @param $member_id
	 * @param $group_id
	 * @param $event_id
	 * @return boolean
	 */
	public static function isEventCrew($member_id, $group_id, $event_id) {
		self::checkInit();
		
		$query = "SELECT count(*) FROM ".self::$db->addPrefix("crew").
			" WHERE member_id=".self::$db->quote( $member_id ).
			" AND group_id=".self::$db->quote( $group_id ).
			" AND (".
				" event_id=".self::$db->quote( $event_id ).
				" OR event_id=".self::$db->quote( self::$NOT_USED ).
			" )";
		
		$res = self::$db->readQueryValue($query);
		if ($res->isError) {
			self::$err->handleError("CREW_READ_ERROR", $res->errorMsg);
			return array();
		}
		return ($res->result > 0);
	}
	
	//*******************************************************************//
	//**                            Team                               **//
	//*******************************************************************//
	
	/**
	 * Read team list (ordered by description)
	 * @return array of team objects (empty array if nothing found)
	 */
	public static function readTeamList() {
		self::checkInit();
		
		$query = "SELECT * FROM ".self::$db->addPrefix("team").
			" ORDER BY description, id";
		
		$res = self::$db->readQuery($query);
		if ($res->isError) {
			self::$err->handleError("TEAM_READ_ERROR", $res->errorMsg);
			return array();
		}
		return $res->result;		
	}
	
	/**
	 * Create a team. A team is just a help during crew assignment.
	 * @param $team object with key fields and required fields [id]
	 */
	public static function createTeam($team) {
		self::checkInit();
		if (is_null($team->id)) {
			self::$err->handleError("NULL_FIELD_ERROR", print_r($team, true));
			return null;
		}
		
		$query = "INSERT INTO ".self::$db->addPrefix("team").
			" (id, description, changed, created)".
			" VALUES (".self::$db->quote( $team->id ).", ".
			self::$db->quote( $team->description ).", NULL, NULL)";
			
		$res = self::$db->writeQuery($query);
		if ($res->isError) {
			self::$err->handleError("TEAM_CREATE_ERROR", $res->errorMsg);
			return null;
		}
	}
	
	/**
	 * Permanently delete a team.
	 * @param $team object with key field [id]
	 */
	public static function deleteTeam($team) {
		self::checkInit();
		if (is_null($team->id)) {
			self::$err->handleError("NULL_FIELD_ERROR", print_r($team, true));
			return null;
		}
		
		$query = "DELETE FROM ".self::$db->addPrefix("team").
			" WHERE id=".self::$db->quote( $team->id );
			
		$res = self::$db->writeQuery($query);
		if ($res->isError) {
			self::$err->handleError("TEAM_DELETE_ERROR", $res->errorMsg);
			return null;
		}
		else if ($res->result == 1) {
			// Delete team memberships
			self::deleteTeamMemberByFields($team->id);
		}
	}
	
	//*******************************************************************//
	//**                         Team member                           **//
	//*******************************************************************//
	
	
	//*******************************************************************//
	//**                         AutoSignUp                            **//
	//*******************************************************************//
	
	/**
	 * Read auto signup list (ordered by event_type, event_subtype, category)
	 * @param $member_id
	 * @param $event_type (optional)
	 * @param $event_subtype (optional)
	 * @param $category (optional)
	 * @return array of auto-signup objects (empty array if nothing found)
	 */
	public static function readAutoSignUpList($member_id, $event_type=null, $event_subtype=null, $category=null) {
		self::checkInit();
		
		$query = "SELECT * FROM ".self::$db->addPrefix("autosignup").
			" WHERE member_id=".self::$db->quote( $member_id ).
			(is_null($event_type) ? "" : " AND event_type=".self::$db->quote( $event_type )).
			(is_null($event_subtype) ? "" : " AND event_subtype=".self::$db->quote( $event_subtype )).
			(is_null($category) ? "" : " AND category=".self::$db->quote( $category )).
			" ORDER BY event_type, event_subtype, category";
		
		$res = self::$db->readQuery($query);
		if ($res->isError) {
			self::$err->handleError("AUTOSIGNUP_READ_ERROR", $res->errorMsg);
			return array();
		}
		return $res->result;		
	}
	
	/**
	 * Read auto signup list (ordered by member_id, event_type, event_subtype, category)
	 * @param $event_type
	 * @param $event_subtype (optional)
	 * @param $category (optional)
	 * @return array of auto-signup objects (empty array if nothing found)
	 */
	public static function readAutoSignUpListAllMembers($event_type, $event_subtype=null, $category=null) {
		self::checkInit();
		
		$query = "SELECT * FROM ".self::$db->addPrefix("autosignup").
			" WHERE event_type=".self::$db->quote( $event_type ).
			(is_null($event_subtype) ? "" : " AND event_subtype=".self::$db->quote( $event_subtype )).
			(is_null($category) ? "" : " AND category=".self::$db->quote( $category )).
			" ORDER BY member_id, event_type, event_subtype, category";
		
		$res = self::$db->readQuery($query);
		if ($res->isError) {
			self::$err->handleError("AUTOSIGNUP_READ_ERROR", $res->errorMsg);
			return array();
		}
		return $res->result;		
	}
	
	/**
	 * Create automatic signup
	 * @param $autoSignUp object with key fields and required fields [member_id, event_type, event_subtype, category, participants]
	 */
	public static function createAutoSignUp($autoSignUp) {
		self::checkInit();
		if (is_null($autoSignUp->member_id) || is_null($autoSignUp->event_type) || is_null($autoSignUp->event_subtype) || is_null($autoSignUp->category) || is_null($autoSignUp->participants)) {
			self::$err->handleError("NULL_FIELD_ERROR", print_r($autoSignUp, true));
			return null;
		}
		
		$query = "INSERT INTO ".self::$db->addPrefix("autosignup").
			" (member_id, event_type, event_subtype, category, participants, changed, created)".
			" VALUES (".self::$db->quote( $autoSignUp->member_id ).", ".
			self::$db->quote( $autoSignUp->event_type ).", ".
			self::$db->quote( $autoSignUp->event_subtype ).", ".
			self::$db->quote( $autoSignUp->category ).", ".
			self::$db->quote( $autoSignUp->participants ).", NULL, NULL)";
			
		$res = self::$db->writeQuery($query);
		if ($res->isError) {
			self::$err->handleError("AUTOSIGNUP_CREATE_ERROR", $res->errorMsg);
			return null;
		}
	}
	
	/**
	 * Update automatic signup (only changeable value is participants)
	 * @param $autoSignUp object with key fields and required fields [member_id, event_type, event_subtype, category, participants]
	 */
	public static function updateAutoSignup($autoSignUp) {
		self::checkInit();
		if (is_null($autoSignUp->member_id) || is_null($autoSignUp->event_type) || is_null($autoSignUp->event_subtype) || is_null($autoSignUp->category) || is_null($autoSignUp->participants)) {
			self::$err->handleError("NULL_FIELD_ERROR", print_r($autoSignUp, true));
			return null;
		}
		
		$query = "UPDATE ".self::$db->addPrefix("autosignup").
			" SET participants=".self::$db->quote( $autoSignUp->participants ).
			", changed=NULL WHERE member_id=".self::$db->quote( $autoSignUp->member_id ).
			" AND event_type=".self::$db->quote( $autoSignUp->event_type ).
			" AND event_subtype=".self::$db->quote( $autoSignUp->event_subtype ).
			" AND category=".self::$db->quote( $autoSignUp->category );
		
		$res = self::$db->writeQuery($query);
		if ($res->isError) {
			self::$err->handleError("AUTOSIGNUP_UPDATE_ERROR", $res->errorMsg);
			return null;
		}
	}

	//*******************************************************************//
	//**                           SignUp                              **//
	//*******************************************************************//
	
	/**
	 * Read signup list (ordered by event_id, member id, category)
	 * @param $group_id
	 * @param $event_id (optional)
	 * @param $member_id (optional)
	 * @param $category (optional)
	 * @return array of signup objects (empty array if nothing found)
	 */
	public static function readSignUpList($group_id, $event_id=null, $member_id=null, $category=null) {
		self::checkInit();
		
		$query = "SELECT * FROM ".self::$db->addPrefix("signup").
			" WHERE group_id=".self::$db->quote( $group_id ).
			(is_null($event_id) ? "" : " AND event_id=".self::$db->quote( $event_id )).
			(is_null($member_id) ? "" : " AND member_id=".self::$db->quote( $member_id )).
			(is_null($category) ? "" : " AND category=".self::$db->quote( $category )).
			" ORDER BY event_id, member_id, category";
		
		$res = self::$db->readQuery($query);
		if ($res->isError) {
			self::$err->handleError("SIGNUP_READ_ERROR", $res->errorMsg);
			return array();
		}
		return $res->result;		
	}
	
	/**
	 * Create a signup
	 * @param $signUp object with key fields and required fields [member_id, group_id, event_id, category, participants]
	 */
	public static function createSignUp($signUp) {
		self::checkInit();
		if (is_null($signUp->member_id) || is_null($signUp->group_id) || is_null($signUp->event_id) || is_null($signUp->category) || is_null($signUp->participants)) {
			self::$err->handleError("NULL_FIELD_ERROR", print_r($signUp, true));
			return null;
		}
		
		$event = self::readEvent($signUp->event_id, $signUp->group_id);
		if (is_null($event)) {
			self::$err->handleError("SIGNUP_EVENT_NOT_FOUND_ERROR", print_r($event, true));
			return null;
		}
		if ($event->status != self::STATUS_OPEN) {
			self::$err->handleError("SIGNUP_EVENT_NOT_OPEN_ERROR", print_r($event, true));
			return null;
		}
		
		$query = "INSERT INTO ".self::$db->addPrefix("signup").
			" (member_id, group_id, event_id, category, participants, changed, created)".
			" VALUES (".self::$db->quote( $signUp->member_id ).", ".
			self::$db->quote( $signUp->group_id ).", ".
			self::$db->quote( $signUp->event_id ).", ".
			self::$db->quote( $signUp->category ).", ".
			self::$db->quote( $signUp->participants ).", NULL, NULL)";
			
		$res = self::$db->writeQuery($query);
		if ($res->isError) {
			self::$err->handleError("SIGNUP_CREATE_ERROR", $res->errorMsg);
			return null;
		}
	}
	
	/**
	 * Update signup (only changeable value is participants)
	 * @param $signUp object with key fields and required fields [member_id, group_id, event_id, category, participants]
	 */
	public static function updateSignup($signUp) {
		self::checkInit();
		if (is_null($signUp->member_id) || is_null($signUp->group_id) || is_null($signUp->event_id) || is_null($signUp->category) || is_null($signUp->participants)) {
			self::$err->handleError("NULL_FIELD_ERROR", print_r($signUp, true));
			return null;
		}
				
		$event = self::readEvent($signUp->event_id, $signUp->group_id);
		if (is_null($event)) {
			self::$err->handleError("SIGNUP_EVENT_NOT_FOUND_ERROR", print_r($event, true));
			return null;
		}
		if ($event->status != self::STATUS_OPEN) {
			self::$err->handleError("SIGNUP_EVENT_NOT_OPEN_ERROR", print_r($event, true));
			return null;
		}
		
		$query = "UPDATE ".self::$db->addPrefix("signup").
			" SET participants=".self::$db->quote( $signUp->participants ).
			", changed=NULL WHERE member_id=".self::$db->quote( $signUp->member_id ).
			" AND group_id=".self::$db->quote( $signUp->group_id ).
			" AND event_id=".self::$db->quote( $signUp->event_id ).
			" AND category=".self::$db->quote( $signUp->category );
		
		$res = self::$db->writeQuery($query);
		if ($res->isError) {
			self::$err->handleError("SIGNUP_UPDATE_ERROR", $res->errorMsg);
			return null;
		}
	}
	
	/**
	 * For a given event, find all members that have automatic signup for this type and subtype
	 * of event (in all categories with autosignup) and create signup accordingly.
	 * @param $event object with required fields [id, group_id, type, subtype]
	 */
	public static function createSignUpsFromAutoSignUps($event) {
		$autoSignUpList = self::readAutoSignUpListAllMembers($event->type, $event->subtype);
		foreach ($autoSignUpList as $autoSignUp) {
			$signUp = new cmeals_std_class();
			$signUp->member_id = $autoSignUp->member_id;
			$signUp->group_id = $event->group_id;
			$signUp->event_id = $event->id;
			$signUp->category = $autoSignUp->category;
			$signUp->participants = $autoSignUp->participants;
			self::createSignUp($signUp);
		}
	}
	
	public static function deleteSignUpByFields($group_id, $event_id=null, $member_id=null) {
		self::checkInit();
		if (is_null($group_id)) {
			self::$err->handleError("NULL_FIELD_ERROR", print_r($group_id, true));
			return null;
		}
				
		$query = "DELETE FROM ".self::$db->addPrefix("signup").
			" WHERE group_id=".self::$db->quote( $group_id ).
			(is_null($event_id) ? "" : " AND event_id=".self::$db->quote( $event_id )).
			(is_null($member_id) ? "" : " AND member_id=".self::$db->quote( $member_id ));
			
		$res = self::$db->writeQuery($query);
		if ($res->isError) {
			self::$err->handleError("SIGNUP_DELETE_ERROR", $res->errorMsg);
			return null;
		}
	}

	//*******************************************************************//
	//**                           Member                              **//
	//*******************************************************************//
	
	private static $memberListCached = NULL;
	
	/**
	 * Read member list (ordered by id)
	 * @return array of member objects (empty array if nothing found)
	 */
	public static function readMemberList() {
		self::checkInit();
		if (!is_null(self::$memberListCached)) return self::$memberListCached;
		
		$query = "SELECT * FROM ".self::$db->addPrefix("member").
			" ORDER BY id";
		
		$res = self::$db->readQuery($query);
		if ($res->isError) {
			self::$err->handleError("MEMBER_READ_ERROR", $res->errorMsg);
			return array();
		}
		self::$memberListCached = $res->result;
		return $res->result;		
	}
	
	/**
	 * Read member
	 * @param $id
	 * @return a member object (null if nothing found)
	 */
	public static function readMember($id) {
		self::checkInit();

		$query = "SELECT * FROM ".self::$db->addPrefix("member").
			" WHERE id=".self::$db->quote( $id );
		
		$res = self::$db->readQuerySingle($query);
		if ($res->isError) {
			self::$err->handleError("MEMBER_READ_ERROR", "id=".$id.". ".$res->errorMsg);
			return null;
		}

		return $res->result;
	}
	
	/**
	 * Get member id of a front end user. More than one front end user 
	 * may correspond to the same member.
	 * Uses a configurable SQL query to establish the relation from
	 * user name to member id.
	 * The user name will be inserted in the configured SQL expression by
	 * substitution of the placeholder {userName} with the actual user name.
	 * If member id cannot be determined, null is returned.
	 * @param $userName
	 * @return member id as string
	 */
	public static function getMemberIdByUserName($userName) {
		self::checkInit();
		$placeHolder = "{userName}";
		
		// Retrieve SQL query from configuration
		$query = self::$cfg->getProperty("user2member_sql");		
		if (is_null($query) || strpos($query, $placeHolder) === false) {
			return null;
		}
		
		// substitute placeholder {userName}
		$query = str_replace($placeHolder, $userName, $query);
		
		// evaluate query
		$res = self::$db->readQueryValue($query);
		if ($res->isError) {
			self::$err->handleError("MEMBER_READ_ERROR", "Query resulted in error: ".$query.". ".$res->errorMsg);
			return null;
		}
		$id = $res->result;
		
		// check that this is a valid member id
		$member = self::readMember($id);
		if (!is_null($member) && $member->id == $id) {
			return $id;
		}
		else {
			return null;
		}
		
	}	
	
	//*******************************************************************//
	//**                         Event Type                            **//
	//*******************************************************************//
	
	/**
	 * Read list of event types
	 * So far, this is just implemented as a stub (no actual table behind)
	 * @return array of event type objects (empty array if nothing found)
	 */
	public static function readEventTypeList() {
		$resultArray = array();
		$typeIdList = array(self::TYPE_COMMUNAL_MEAL);
		foreach ($typeIdList as $id) {
			$eventType = self::readEventType($id);
			if (!is_null($eventType)) {
				$resultArray[] = $eventType;
			}
		}
		return $resultArray;		
	}
	
	/**
	 * Read event type
	 * So far, this is just implemented as a stub (no actual table behind)
	 * @param $id e.g. CMeals::TYPE_COMMUNAL_MEAL
	 * @return event type object (null if nothing found)
	 */
	public static function readEventType($id) {
		$eventType = null;
		if ($id == self::TYPE_COMMUNAL_MEAL) {
			$eventType = new cmeals_std_class();
			$eventType->id = self::TYPE_COMMUNAL_MEAL;
			$eventType->display_name = "COMMUNAL_MEAL";
		}
		return $eventType;		
	}
	
	//*******************************************************************//
	//**                       Event Subtype                           **//
	//*******************************************************************//
	
	/**
	 * Read list of subtypes for a given type of event
	 * So far, this is just implemented as a stub (no actual table behind)
	 * @param $event_type e.g. CMeals::TYPE_COMMUNAL_MEAL
	 * @return array of event subtype objects (empty array if nothing found)
	 */
	public static function readEventSubTypeList($event_type) {
		$resultArray = array();
		if ($event_type == self::TYPE_COMMUNAL_MEAL) {
			$subtypes = array("MO"=>"MONDAY", "TU"=>"TUESDAY", "WE"=>"WEDNESDAY", "TH"=>"THURSDAY", "FR"=>"FRIDAY", "SA"=>"SATURDAY", "SU"=>"SUNDAY");
			foreach (array_keys($subtypes) as $id) {
				$eventSubtype = new cmeals_std_class();
				$eventSubtype->id = $id;
				$eventSubtype->event_type = $event_type;
				$eventSubtype->display_name = $subtypes[$id];
				$resultArray[] = $eventSubtype;
			}
		}
		return $resultArray;		
	}
	
	//*******************************************************************//
	//**                       Event Category                          **//
	//*******************************************************************//
	
	/**
	 * Read list of categories for a given type of event
	 * So far, this is just implemented as a stub (no actual table behind)
	 * @param $event_type e.g. CMeals::TYPE_COMMUNAL_MEAL
	 * @param $event_subtype (optional)
	 * @return array of event category objects (empty array if nothing found)
	 */
	public static function readEventCategoryList($event_type, $event_subtype=null) {
		if (is_null($event_subtype)) {
			$event_subtype = self::$NOT_USED;
		}
		$resultArray = array();
		if ($event_type == self::TYPE_COMMUNAL_MEAL) {
			$categories = array("AD" => "ADULT", "CH" => "CHILD", "IN" => "INFANT");
			foreach (array_keys($categories) as $id) {
				$category = new cmeals_std_class();
				$category->id = $id;
				$category->event_type = $event_type;
				$category->event_subtype = $event_subtype;
				$category->display_name = $categories[$id];
				$resultArray[] = $category;
			}
		}
		return $resultArray;		
	}
	
}

?>